feat(email): follow-up tracking — flag sent mail awaiting a reply#1922
feat(email): follow-up tracking — flag sent mail awaiting a reply#1922kovtcharov wants to merge 5 commits into
Conversation
Plan for triaging personal Gmail through the Agent UI with two interchangeable sidecar backends behind one fixed REST seam: a frozen npm binary for users and a hot-reloading uvicorn-from-source server for developers. Both run the same agent code against the same local Google grant, so personal email works identically in each and improving the agent is a one-file edit with no freeze/publish/patch loop.
The first draft assumed the renderer calls a fixed /v1/email REST URL and that an Electron-main sidecar manager would host it. Code review found the UI surfaces email through the chat agent loop + SSE cards (the frontend has no email REST client), and the agent already runs in-process in the Python backend reading ~/.gaia/connectors/grants.json. Reframe: the seam is the chat agent's email-tool layer and the host is the Python UI backend (no Node backend needed). Both modes proxy over the same HTTP path to a sidecar on :8131 — frozen binary for users, uvicorn --reload from source for devs — keeping process isolation identical across modes and preserving the existing SSE card pipeline.
Fold in the two product requirements and resolve every open item from the architecture review: - Dogfooding: user mode runs the exact Hub-published frozen binary (pinned via binaries.lock.json), so the Agent UI validates the shipped product. This is the rationale the sidecar earns over an editable in-process install. - Lightweight UI: the sidecar is lazy-fetched on first email use, one platform binary only; core ships no email deps. - Resolve open questions: use a proxy AGENT reusing agent_type=email machinery; remove the redundant in-process /v1/email mount (#1768) so the UI has one backend. - Review fixes: own the dev-mode enabler (module-level app = build_app()), reuse the npm fetch SHA/lock integrity check instead of porting it to Python, ephemeral per-instance port (no fixed-8131 multi-instance clash), single-writer grant model (OAuth stays on backend; sidecar reads only), exact current-contract table. - Sequence behind the in-flight REST build-out (pre-scan + #1779/#1780/#1781) and flag the #1767 in-process decision must be superseded with @itomek sign-off.
…tion) itomek closed #1767/#1785 because mixing an npm package requiring Node.js into the Python-backed Agent UI is too dirty. Remove that entirely: the Python backend fetches the binary itself (verified SHA-256 download, no npx), spawns it, and proxies over HTTP. No Node runtime, no npm package in the UI's path. The npm package remains the external Node-integrator channel only.
) The unanswered email you already sent is the inbox's biggest silent failure mode — until now the agent had no way to surface it. A new read-only check_followups tool scans the Sent folder of every connected mailbox and flags threads whose latest message is still the user's own outbound mail past a configurable window (default 3 days, EmailAgentConfig.followup_window_days), returning message id, recipient, subject, and age — most overdue first. Detection only: it never drafts or sends a nudge; sends stay confirmation-gated (#1264) and autonomous follow-ups remain #555's scope. The Outlook backend gains a SENT branch in list_messages (Graph sentitems folder) — an unknown label previously fell through to the inbox folder, which would have made a Sent-folder scan silently read the wrong mail — and the detector accepts both epoch-millis and ISO-8601 internalDate values so Gmail and Outlook threads age identically.
|
Verdict: Approve with suggestions — no blocking issues. This PR adds a read-only The one thing worth weighing before release: because the Sent folder is scanned newest-first and capped (default 50, max 200), a user with high send volume will have their oldest — i.e. most overdue — sends fall off the end of the scan. For a feature whose whole pitch is "resurface the dropped thread you forgot about," that trade-off deserves a line of user-facing docs. It's bounded and disclosed in the tool's docstring, so it's not a blocker. The live-mailbox smoke test is correctly flagged as unrun (needs a hardware pass) — appropriately disclosed, not a merge blocker for code review. 🔍 Technical details🟢 Minor / nits1. Newest-first scan + 2. Recipient is derived from 3. Outlook Sent scan orders by Process note (not a code issue): Strengths
|
Closes #1606
Why this matters
Before: an email you sent that never got a reply just disappeared — the agent had no way to resurface a dropped thread, the inbox's biggest silent failure mode. After: asking "which of my sent emails are still waiting on a reply?" runs a new read-only
check_followupstool that scans the Sent folder of every connected mailbox and returns each thread still awaiting an inbound reply past a configurable window (default 3 days) — recipient, subject, and age, most overdue first. Detection only: the tool never drafts or sends a nudge (sends stay confirmation-gated per #1264; autonomous follow-ups remain #555's scope), and that read-only guarantee is locked in by tests both dynamically (backend call log) and statically (module source).Also fixes a latent trap this feature would have stepped on: the Outlook backend silently mapped a
SENTlabel to the inbox folder, so a Sent-folder scan against Outlook would have quietly scanned the wrong mail. It now routes to Graph'ssentitems, and the detector accepts both Gmail's epoch-millis and Outlook's ISO-8601internalDate.Test plan
python -m pytest hub/agents/python/email/tests/test_email_followups.py— 15 passed: reply-after-send suppresses the flag, no-reply past window flags (message_id/recipient/subject/age), within-window doesn't, reply before the send doesn't count, one flag per thread, self-addressed skipped, ISO-8601 dates, sort order, loud errors on invalid window, read-only guard (allowed backend calls only + no send/draft references in module source), tool envelope + mailbox tagging.python -m pytest tests/unit/agents/email/ tests/unit/agents/test_email_agent.py tests/unit/email/ hub/agents/python/email/tests/— 747 passed; the two failures (test_no_unexpected_tool_set,test_set_priority_sender_rejects_invalid_email) fail identically on a cleanmaincheckout in this environment (machine-local memory/keyring state), verified viagit stash.python util/lint.py --all --fix— clean (black/isort/pylint/flake8 pass).